-- 客户端代理服务
-- Author: Colocasia
-- Date: 2019-06-16

local skynet = require "skynet";
require 'skynet.manager';
local socket = require 'us_socket';
local us_rpc = require 'us_rpc';
local tool = require 'tool';
local timer = require 'timer';
local log = require "log";
local handler = require 'handler';

local string = require 'us.string';

---@class agent
---@field HEARTBEAT_TIME number 心跳超时断开
---@field KEEPALIVE_TIME number 
local agent = {}


local SEND_SESSION_NO_RET = 0
local WATCHDOG;
local GATE;


-- 客户端
local player                -- 玩家
local client_fd;            -- 玩家地址
local client_ip;            -- 玩家端IP
local player_guid;	        -- 玩家GUID
local client_proxy;         -- 玩家代理


local LAST_HEARTBEAT;       -- 上次通信时间
local HEARTBEAT_TIME = 20   -- 没有客户端请求，超时90秒后断开连接
local KEEPALIVE_TIME = 90   -- 没有请求，超时90秒后退出


-- 常用函数引用
local send_request = us_rpc.send_request;

---重置激活时间
local function cmd_reset()
    if not player then return end
    local now = os.time()
    player.close_time = now + KEEPALIVE_TIME
end

---重置心跳时间
local function heartbeat_reset()
    if not player or not client_fd then return end
    local now = os.time();
    player.socket_time = now + HEARTBEAT_TIME;
    cmd_reset();
end

---检查心跳包
local function check_heartbeat()
    -- log.debug("[Agent]check_heartbeat fd: %s", client_fd);
    if not player or not client_fd then return end
    local now = os.time()
    -- log.debug("[Agent]check_heartbeat cur:%s, last:%s", now, player.socket_time);
    if now < player.socket_time then return end
    log.info("[Agent]timeout exit fd: %s", client_fd);
    skynet.send(WATCHDOG, 'lua', 'kick', client_fd);
end

---检查持续活动
local function check_keepalive()
    if not player or not player_guid then return end
    local now = os.time()
    if now < player.close_time then return end
    log.info("[Agent]timeout close agent, player: %s", player_guid);
    skynet.send(WATCHDOG, 'lua', 'close_agent', player_guid);
end

---执行命令
local function do_cmd(cmd, ...)
    local player = client_proxy:entity();
    assert(player and cmd)
    cmd_reset()
    log.debug('[Agent]execute user_cmd, cmd = [%s]', cmd);
    local func = player[cmd]
    assert(func)
    return func(player, ...)
end

-- 发送回应
local function send_package(fd, pack)
    if not fd then return end
    socket.send(fd, pack)
end

-- 处理请求
local function request(fd, name, msg, response)
    if not player_guid or not client_proxy then
        log.info('no user_id with fd = %s, CLIENT-MSG request = %s', fd, name);
        return;
    end
    log.info(string.format("CLIENT-MSG request = %s", name));
    -- 重置心跳时间
    heartbeat_reset();
    local cmd = client_proxy[name]
    local f = assert(cmd, string.format("client_proxy cmd=>%s not found", cmd))
    local r = f(client_proxy, msg) or {}
    if response then
        log.info(string.format("CLIENT-MSG response = %s", name));
        return response(r);
    else
        log.info(string.format("CLIENT-MSG ERROR, NO RESPONSE = %s", name));
    end
    return response(handler:render({}));
end

-- 发送推送消息
local function send_push(fd, msg)
    --assert(fd, "invalid fd!");
    if not fd then return end
    local pack = send_request(msg);
    log.info("send_push fd->%s, pack->%s", fd, pack);
    send_package(fd, pack);
end
agent.send_push = send_push;

---玩家存盘
local function update()
    check_heartbeat();
    check_keepalive();
end

---启动客户端代理
function agent.start(conf)
    -- 转发客户端消息
    GATE = conf.gate;
    WATCHDOG = conf.watchdog;

    -- 创建玩家数据
	player_guid = conf.guid;
    client_proxy = require('client_proxy').new(player_guid);
    player = client_proxy:entity();

    -- 登录时重置心跳时间
    timer.del_timer("agent_update");
    timer.add_timer("agent_update", 1, true, update);

    if not conf.fd then return end
    agent.enter(conf);
end

---玩家附加到代理
function agent.enter(conf)
    local fd = conf.client
    local ip = tool:ip(conf.addr)
    log.info('[agent]client enter ip->%s fd->%s', ip.v4 or ip.v6, fd);

    client_fd = fd;
    client_ip = ip;

    skynet.call(GATE, "lua", "forward", fd, fd);
    heartbeat_reset();

    player:set_push_info(client_fd, send_push);
end

---客户端代理登录
function agent.login()
    if not player then return {} end
    return player:login();
end

---客户端代理心跳
function agent.heartbeat()
    heartbeat_reset();
end

---玩家从客户端代理退出
function agent.exit()
    log.info('[agent]client exit ip->%s fd->%s', client_ip.v4 or client_ip.v6, client_fd);
    client_fd = nil;
end

---客户端代理关闭
function agent.close()
    -- todo: do something before exit
    skynet.exit()
end

---执行玩家指令
function agent.exc_user_cmd(cmd, ...)
    return do_cmd(cmd, ...);
end

---发送玩家指令
function agent.send_user_cmd(cmd, ...)
    do_cmd(cmd, ...);
end

-- 错误处理
function on_cmd_err(msg)
    log.error(msg);
end

-- 代理初始化
local recv;
function agent.init()
    skynet.dispatch("lua", function(session, source, cmd, ...)
        -- skynet.trace()
        local f = assert(agent[cmd], string.format("agent cmd=>%s not found", cmd));
        local function ret(ok, ...)
            if session ~= SEND_SESSION_NO_RET then
                skynet.retpack(...)
            end
        end
        ret(xpcall(f, on_cmd_err, ...));
    end)

    skynet.register_protocol {
        name = "client",
        id = skynet.PTYPE_CLIENT,
        unpack = function (msg, sz)
            recv = skynet.time();
            buffer = skynet.tostring(msg, sz);
            return us_rpc:unpack(buffer, sz)
        end,
        dispatch = function (fd, _, type, ...)
            skynet.ignoreret()	-- session is fd, don't call skynet.ret
            -- skynet.trace()
            if 1 == type then       -- 请求
                local ok, result = xpcall(request, on_cmd_err, fd, ...);
                if ok then
                    if result then
                        send_package(fd, result)
                        log.fatal("cost->%s, recv->%s, send->%s", skynet.time()-recv, skynet.time(), recv);
                    end
                else
                    skynet.error(result)
                end
            else					-- 回应
                
            end
        end
    }
end

skynet.start(agent.init);
